Skip to content

Conversation

@jdoucett
Copy link
Contributor

Typed reclaim holds + hardened snapshot guard

Replaces the flat reclaim_holds.txt with typed holds so a source is held by why,
and hardens the snapshot recoverability check. Engine side only (this repo); the receipt
writer + Teams review card are florasense-tools follow-ups. Based on main, not stacked.
Suite 196 → 222 green. Legacy .txt holds are unchanged (each line = a business hold).

Tiers (verdicts). No auto-reclaim — reclaim is report-only unless --delete (which confirms per folder).

  • businessHOLD — a human owns it; never reclaimed.
  • promote-pendingHOLD until a content-coverage receipt proves the copy landed
    in repository/, then AWAITING-APPROVAL (eligible; a human runs the guarded delete).
  • superseded (DECISIONS feat(restore): Phase-2 full restore capstone (slice 5) #25 / florasense PR #70) → HOLD until a supersession
    provenance record
    exists, then AWAITING-APPROVAL. The record is human-authored (its
    MD5 verdict is human judgment the engine trusts); the engine enforces the machine-checkable
    gate feat(restore): Phase-2 full restore capstone (slice 5) #25 requires — the record is present, non-empty, and filled in (not a raw template).

AWAITING-APPROVAL is the new "needs approval" verdict the Teams review card (Step C) will
surface. Nothing un-promoted / un-recorded is ever eligible → HOLD, full stop.

Hardened snapshot guard

snapshot_present now requires every live file present in the newest NetApp snapshot with
matching size and mtime
, not a file-count proxy — which silently passed a file
replaced since the snapshot (same count → old bytes) or renames. Test
test_replaced_file_same_count_fails locks the fix.

Removed

The auto_reclaim flag + AUTO_RECLAIM_ENABLED switch — dead/dormant machinery. "Don't
reclaim by default" is structural (report-only + --delete + per-folder confirm), so there's
nothing to disable. A stray auto_reclaim field is ignored (test).

Files

  • reclaim.pyload_holds (JSON/typed + legacy .txt), hold_entry_for (str|dict),
    receipt_valid, supersession_record_valid, classify_hold (the tiers),
    AWAITING_APPROVAL verdict, hardened snapshot_present (_attr_catalog), main() order.
  • docs/HOLDS_POLICY.md — the policy, JSON schema, hard gates, and alignment to the deployed
    florasense gates (fs-verify-promoted receipts, fs-coverage-audit blind-spot watchdog).
  • tests/test_typed_holds.py — 26 tests.

Follow-ups (not this PR)

🤖 Generated with Claude Code

…d snapshot guard

Typed holds replace the flat reclaim_holds.txt, yielding the three review tiers Jarrod
asked for. Auto-reclaim DISABLED (AUTO_RECLAIM_ENABLED=False): nothing ever auto-deletes.

- load_holds: reads reclaim_holds.json (typed) or legacy .txt (each line = a business
  hold, so existing deployments are unchanged). hold_entry_for tolerates str or dict.
- Tiers via classify_hold: business -> HOLD; promote-pending + no valid receipt -> HOLD
  (pending); promote-pending + valid receipt -> new AWAITING-APPROVAL verdict (eligible,
  human approves). receipt_valid (content-agnostic): newest receipt for the source, valid
  iff all_present AND file_count==live AND nothing newer than verified_at.
- verify() tail now tiers via the matched hold entry (was a flat 'held -> HOLD').
- snapshot_present HARDENED: every live file must be in the newest snapshot with matching
  size AND mtime (was a file-count >= proxy that missed a same-count replace/rename).
- docs/HOLDS_POLICY.md; nothing un-promoted is ever reclaimed (receipt = promote proof);
  hard gates (tape + canonical + snapshot + path) still apply to any --delete.

Tests: tests/test_typed_holds.py (23). Suite 196 -> 219. Engine side only; the receipt
writer (fs-verify-promoted) + the review-only Teams card are florasense-tools follow-ups.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…#70, fs-coverage-audit #71)

Point HOLDS_POLICY.md at the two florasense-tools gates that wrap engine reclaim:
- supersession-record gate (DECISIONS #25 / data-supersession.md) required before deleting
  a SUPERSEDED copy (vs routine reclaim); note a future reason:superseded tier could let the
  engine enforce it (analogous to promote-pending + receipt).
- fs-coverage-audit covers reclaim's never-archived/single-copy blind spot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Jarrod: 'why have an auto-reclaim that's disabled rather than just defaulting to not
reclaim?' Right — reclaim is already report-only (nothing deletes without --delete, which
confirms per folder), so 'don't reclaim by default' is structural. Removed the
AUTO_RECLAIM_ENABLED switch + the auto_reclaim flag handling + the SAFE-via-auto branch;
promote-pending + valid receipt is always AWAITING-APPROVAL. Two live outcomes: Hold,
Needs-approval. A stray auto_reclaim field is ignored (test). Docs + docstring updated;
if unattended sweeping is ever wanted it's a deliberate future feature, not a dormant default.

Suite still 219.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-record gate

Adds a third typed-hold reason, 'superseded' (florasense DECISIONS #25 / PR #70): a
corrected/re-delivered/reprocessed/deduped copy may be deleted only once a supersession
provenance record exists. supersession_record_valid checks the record (entry['record'],
normally the repository provenance/ copy which persists under the canonical root) is
present, non-empty, and filled in (not a raw template — footgun guard). Content (the MD5
verdict) is human judgment the engine trusts; existence is the machine-checkable gate #25
requires. classify_hold: superseded + no record -> HOLD (pending record); + valid record
-> AWAITING-APPROVAL (human approves the delete). Mirrors promote-pending + receipt.

Tests +3 (no record / valid record / unfilled template). Suite 219 -> 222. HOLDS_POLICY.md
tier table + JSON example (X0H Soy AAPF worked example) + deployment-gates note updated
(the engine now enforces #25's existence gate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Jarrod S Doucette (jdoucett) added a commit that referenced this pull request Jul 10, 2026
…holds)

Merging slice-3 (#21, verify_target) and typed holds (#22) composed a latent bug: #22 makes
load_holds return typed dict entries, but verify_target still did inline string matching
(h in source_folder), which raises TypeError on a dict hold — i.e. verify_target would crash
on any routed target the moment a reclaim_holds.json is used. Legacy .txt (string) holds were
unaffected, so the merged suite stayed green and it went unseen.

Route verify_target through the same hold_entry_for + classify_hold helpers verify() uses, so
routed targets get the full business/promote-pending/superseded tiering (HOLD / AWAITING-
APPROVAL) instead of a flat crash-prone string match. Tests: verify_target under a business
dict hold -> HOLD, promote-pending no-receipt -> HOLD, promote-pending valid-receipt ->
AWAITING-APPROVAL. Suite 235 -> 238.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jdoucett Jarrod S Doucette (jdoucett) deleted the feat/typed-holds branch July 10, 2026 02:22
Jarrod S Doucette (jdoucett) added a commit that referenced this pull request Jul 10, 2026
…typed-holds

fix(reclaim): verify_target tiers typed holds (composition bug from #21+#22)
Sign in to join this conversation on GitHub.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant